<?php

/**
 * frameworky.php - non-obtrusive PHP website framework
 *
 * Frameworky is a website framework that strives to be as non-invasive and
 * non-intrusive to the development process as possible.
 *
 * Core Principles:
 *     security: protect sensitive data from theft
 *     urls: support search/visitor friendly urls
 *     caching: support intelligent content caching
 *     environments: support multiple websites with one upgradable core
 *     data access: responsibly access data from server-side and client-side
 *     data backup: easily backup/restore source code and data
 *     full control: override native behaviors
 *     transparency: allow total developmental freedom (i.e. static, procedural,
 *         mvc, active record, custom extensions, custom templating languages,
 *         and so on)
 *
 * Frameworky will not make you a better developer, but it will not hinder you
 * from becoming one either.
 *
 * Note: This class depends on PHP5, Apache, mod_rewrite, GD library, and MySQL
 * which are available on almost all linux hosting environments. The main focus
 * here was to support web application development on affordable hosting
 * solutions.
 *
 * Workflow:
 *     Load configurations
 *     Find requested uri
 *     Dissect requested uri
 *     Look for rewrite rules to determine the real url
 *     Rebuild the dissected url using the rewrite rules
 *     Look for cache rules to determine if this output should be cached
 *     If caching, enable output buffer
 *     Process real url
 *     If caching, save output buffer to static file and return file results
 *
 * @author Toby Miller <tobius.miller@gmail.com>
 * @copyright 2008 Toby Miller <tobius.miller@gmail.com>
 * @license http://opensource.org/licenses/mit-license.php MIT License
 */

class Frameworky {
	public static $config = null;
	public static $firephp = null;
	public static $requested_uri = null;
	public static $processed_uri = null;
	public static $vars = array();
	public static $binary = array(
		'doc' => 'application/msword',
		'bin' => 'application/octet-stream',
		'exe' => 'application/octet-stream',
		'class' => 'application/octet-stream',
		'so' => 'application/octet-stream',
		'dll' => 'application/octet-stream',
		'pdf' => 'application/pdf',
		'eps' => 'application/postscript',
		'ps' => 'application/postscript',
		'xls' => 'application/vnd.ms-excel',
		'ppt' => 'application/vnd.ms-powerpoint',
		'sit' => 'application/x-stuffit',
		'tgz' => 'application/x-tar',
		'tar' => 'application/x-tar',
		'zip' => 'application/zip',
		'jpg' => 'image/jpeg',
		'jpeg' => 'image/jpeg',
		'gif' => 'image/gif',
		'png' => 'image/png',
		'ico' => 'image/x-icon',
		'mov' => 'video/quicktime',
		'qt' => 'video/quicktime',
		'mp3' => 'audio/mpeg',
		'mp4' => 'application/x-shockwave-flash',
		'avi' => 'application/x-shockwave-flash',
		'swf' => 'application/x-shockwave-flash'
	);
	public static $ascii = array(
		'js' => 'application/x-javascript',
		'json' => 'text/x-json',
		'css' => 'text/css',
		'rdf' => 'application/rdf+xml',
		'xul' => 'application/vnd.mozilla.xul+xml',
		'wbxml' => 'application/vnd.wap.wbxml',
		'wmlc' => 'application/vnd.wap.wmlc',
		'wmlsc' => 'application/vnd.wap.wmlscriptc',
		'vxml' => 'application/voicexml+xml',
		'htm' => 'text/html',
		'html' => 'text/html',
		'php' => 'text/html',
		'php3' => 'text/html',
		'php4' => 'text/html',
		'phtml' => 'text/html',
		'xml' => 'application/xml',
		'xsl' => 'application/xml',
		'dtd' => 'application/xml-dtd',
		'xslt' => 'application/xslt+xml'
	);

	/**
	 * run this request through the processor
	 *
	 * @param string uri
	 * @return void
	 */
	public static function run($uri){
		self::load_configuration($uri);
		if (self::$config['host_dir'] != null){
			if (is_file(self::$config['host_dir'] . self::$processed_uri['path'])){
				$custom = array(self::$config['host_dir'] . '/.conf');
				$path = preg_replace('/^\/*(.*)\/*$/', '$1', self::$processed_uri['path']);
				if (strstr($path, '/')){
					$paths = explode('/', $path);
					array_pop($paths);
					$path = join('/', $paths);
				} else {
					$path = '';
				}
				$paths = array();
				if ($path != ''){
					while(strstr($path, '/')){
						$paths[] = $path;
						$split = explode('/', $path);
						array_pop($split);
						$path = join('/', $split);
					}
					$paths[] = $path;
				}
				foreach($paths as $path) $custom[] = self::$config['host_dir'] . '/' . $path . '/.conf';
				$custom[] = self::$config['host_dir'] . str_replace(self::$processed_uri['filename'], '.' . self::$processed_uri['filename'], self::$processed_uri['path']) . '.conf';
				sort($custom);
				$configs = array();
				foreach($custom as $config) if (is_file($config)) $configs[] = $config;
				foreach($configs as $config) self::extend_configuration($config);
				self::output_file();
			} else {
				self::output_error('404', 'Cannot find uri ' . self::$processed_uri['path'], __LINE__, __FILE__);
			}
		} else {
			self::output_error('500', 'Invalid Host Directory', __LINE__, __FILE__);
		}
	}

	/**
	 * get the current execution time for this Frameworky session
	 *
	 * @param void
	 * @return void
	 */
	public function get_current_execution_time(){
		$now = microtime_float();
		return($now - $GLOBALS['TIME_START']);
	}

	/**
	 * determine if the requested uri belongs to an image
	 *
	 * @param string uri
	 * @return boolean
	 */
	public function is_image($uri){
		return(preg_match('/^.*\.(jpg|jpeg|gif|png)$/', $uri) ? true : false);
	}

	/**
	 * load configuration settings
	 *
	 * @param string requested uri
	 * @return boolean
	 */
	public function load_configuration($uri){
		if (!preg_match('/^\//', $uri)) $uri = '/' . $uri;
		if ((self::$config != null && self::$requested_uri['uri'] != $uri) || (self::$config == null)){
			$config = array(
				'enabled' => true,
				'debug' => true,
				'host_dir' => null,
				'hosts' => array(),
				'compress_js' => false,
				'compress_css' => false,
				'compress_html' => false,
				'compress_jpeg' => false,
				'jpeg_quality' => 100,
				'databases' => array(),
				'cache_enabled' => false,
				'cache_rules' => array(),
				'rewrite_enabled' => false,
				'rewrite_rules' => array()
			);
			if (isset($GLOBALS['CONFIG']['global'])) $config = array_merge($config, $GLOBALS['CONFIG']['global']);
			while(list($profile, $object) = each($GLOBALS['CONFIG'])){
				if (in_array(getenv('HTTP_HOST'), $GLOBALS['CONFIG'][$profile]['hosts'])){
					$config = array_merge($config, $GLOBALS['CONFIG'][$profile]);
				}
			}
			$src_dir = realpath('./src');
			$config = array_merge($config, array(
				'lib_dir' => realpath('./lib'),
				'tmp_dir' => realpath('./tmp'),
				'data_dir' => realpath('./data'),
				'src_dir' => $src_dir,
				'www_dir' => getenv('DOCUMENT_ROOT'),
				'host_dir' => $src_dir . '/' . $config['host_dir']
			));
			self::$config = $config;
			self::$requested_uri = self::parse_uri($uri);
			self::$processed_uri = self::parse_uri(self::rewrite_uri($uri));
		}
	}

	/**
	 * extend configuration settings
	 *
	 * @param string file path
	 * @return void
	 */
	public function extend_configuration($path){
		if (is_file($path)){
			include($path);
			if (isset($config) && is_array($config)) self::$config = array_merge(self::$config, $config);
		}
	}

	/**
	 * rewrite uri based on configured rewrite rules
	 *
	 * @param string uri
	 * @return string rewritten uri
	 */
	public function rewrite_uri($uri){
		if (self::$config['rewrite_enabled']){
			$old = $uri;
			$new = null;
			$redirect = false;
			$len = count(self::$config['rewrite_rules']);
			for ($i = 0; $i < $len; $i++){
				if (preg_match(self::$config['rewrite_rules'][$i]['match'], $uri)){
					$new = preg_replace(self::$config['rewrite_rules'][$i]['match'], self::$config['rewrite_rules'][$i]['replace'], $uri);
					$redirect = isset(self::$config['rewrite_rules'][$i]['redirect']) ? self::$config['rewrite_rules'][$i]['redirect'] : $redirect;
					$i = $len;
				}
			}
			if ($new != null){
				if ($redirect && ($uri != $new)){
					header('Location: ' . self::get_scheme() . '://' . getenv('HTTP_HOST') . $new);
					exit;
				}
				return($new);
			}
		}
		return($uri);
	}

	/**
	 * parse uri
	 *
	 * returned uri information
	 * [
	 *     url => full requested url,
	 *     uri => full requested uri,
	 *     path => full uri path (after extracting special instructions),
	 *     filename => filename (after extracting special instructions),
	 *     extension => file extension,
	 *     filetype => file type (ascii|binary),
	 *     query => associative array of GET parameters (key/value pairs),
	 *     fragment => uri anchor,
	 *     ttl => time to live in seconds (cache setting)
	 *     special => associative array of special instructions
	 * ]
	 *
	 * note: special instructions are found within a requested filename and
	 * currently only apply to media files, i.e.
	 *     width = _wXXX (resize to XXX pixels wide)
	 *     height = _hXXX (resize to XXX pixels wide)
	 *     quality = _qXXX (resample jpeg to XXX percent quality)
	 *
	 * @param string uri
	 * @return mixed object on success, false on failure
	 */
	public function parse_uri($uri){
		$url = self::get_scheme() . '://' . getenv('HTTP_HOST') . $uri;
		$parsed = parse_url($url);
		if ($parsed !== false){
			$query = parse_url(getenv('REQUEST_URI'));
			if ($query !== false) $parsed['query'] = $query['query'];
			$filename = preg_replace('/^.*\/([^\/]+\.[a-z]+)$/', '$1', $parsed['path']);
			$extension = preg_replace('/^.*\.([a-z]+)$/', '$1', $filename);
			$filetype = self::get_file_type($extension);
			$mimetype = self::get_mime_type($extension);
			$path = $parsed['path'];
			$special = array(
				'width' => -1,
				'height' => -1,
				'quality' => in_array($extension, array('jpg','jpeg')) ? self::$config['jpeg_quality'] : 100
			);
			if (in_array($extension, array('gif','jpg','jpeg','png'))){
				$params = explode('_', preg_replace('/^(.*)\.([a-z]+)$/', '$1', $filename));
				$len = count($params);
				$instructions = array('w' => 'width', 'h' => 'height', 'q' => 'quality');
				for ($i = 0; $i < $len; $i++){
					preg_match_all('/^([a-z])([0-9]+)$/', $params[$i], $match);
					if (isset($match[1][0]) && isset($match[2][0])){
						if (in_array($match[1][0], array_keys($instructions))){
							$special[$instructions[$match[1][0]]] = $match[2][0];
							$filename = str_replace('_' . $params[$i], '', $filename);
							$path = str_replace('_' . $params[$i], '', $path);
						}
					}
				}
			}
			$query = null;
			if ($parsed['query'] != '') parse_str($parsed['query'], $query);
			return(
				array(
					'url' => $url,
					'uri' => $uri,
					'path' => $path,
					'filename' => $filename,
					'extension' => $extension,
					'filetype' => $filetype,
					'mimetype' => $mimetype,
					'query' => $query,
					'fragment' => $parsed['fragment'],
					'ttl' => self::get_ttl($uri),
					'special' => $special
				)
			);
		}
		return(false);
	}

	/**
	 * get time to live (in seconds) for the requested uri
	 *
	 * @param string uri
	 * @return integer seconds
	 */
	public function get_ttl($uri){
		$ttl = -1;
		if (self::$config['cache_enabled']){
			$len = count(self::$config['cache_rules']);
			for ($i = 0; $i < $len; $i++){
				if (preg_match(self::$config['cache_rules'][$i]['match'], $uri)){
					$ttl = self::$config['cache_rules'][$i]['ttl'];
					$i = $len;
				}
			}
		}
		return($ttl);
	}

	/**
	 * get file type
	 *
	 * @param string extension
	 * @return string file type (ascii|binary)
	 */
	public function get_file_type($extension){
		if (in_array($extension, array_keys(self::$binary))){
			return('binary');
		} else {
			return('ascii');
		}
	}

	/**
	 * get mime type
	 *
	 * @param string extension
	 * @return string mime type
	 */
	public function get_mime_type($extension){
		if (in_array($extension, array_keys(self::$binary))){
			return(self::$binary[$extension]);
		} else if (in_array($extension, array_keys(self::$ascii))){
			return(self::$ascii[$extension]);
		} else {
			return('text/plain');
		}
	}

	/**
	 * get scheme
	 *
	 * @param void
	 * @return string scheme
	 */
	public function get_scheme(){
		switch(getenv('SERVER_PORT')){
			case '80':
				return('http');
				break;
			case '443':
				return('https');
				break;
			case '21':
				return('ftp');
				break;
			case '990':
				return('ftps');
				break;
			default:
				return('http');
				break;
		}
	}

	/**
	 * set a variable for use within included php files
	 *
	 * @param string var name
	 * @param mixed var value
	 * @return void
	 */
	public function set($name, $value){
		self::$vars[$name] = $value;
	}

	/**
	 * append a variable to an array for use within included php files
	 *
	 * @param string var name
	 * @param object var value
	 * @return void
	 */
	public function append($name, $value){
		if (!isset(self::$vars[$name])) self::$vars[$name] = array();
		self::$vars[$name][] = $value;
	}

	/**
	 * get a variable for use within included php files
	 *
	 * @param string var name
	 * @return mixed var value
	 */
	public function get($name){
		return(isset(self::$vars[$name]) ? self::$vars[$name] : null);
	}

	/**
	 * get a configuration variable for use within included php files
	 *
	 * @param string var name
	 * @return mixed var value
	 */
	public function get_config($name){
		return(isset(self::$config[$name]) ? self::$config[$name] : null);
	}

	/**
	 * include a file using the current working source tree
	 *
	 * @param string file path
	 * @return void
	 */
	public function inc($file_path){
		$GLOBALS['DEBUG'] .= "inc($file_path)<br/>\n";
		if (self::$config['host_dir'] != null){
			include(self::$config['host_dir'] . '/' . $file_path);
		}
	}

	/**
	 * include a library
	 *
	 * @param string library file
	 * @return void
	 */
	public function lib($library_file){
		if (self::$config['lib_dir'] != null){
			include_once(self::$config['lib_dir'] . '/' . $library_file);
		}
	}

	/**
	 * read a file using the current working source tree
	 *
	 * @param string file path (or url)
	 * @return void
	 */
	public function read($file_path){
		if (preg_match('/^http/', $file_path)){
			return(file_get_contents($file_path));
		} else if (self::$config['host_dir'] != null){
			return(file_get_contents(self::$config['host_dir'] . '/' . $file_path));
		}
	}

	/**
	 * send a messages to a logging mechanism
	 *
	 * note: message type #2 is no longer used by PHP so we use it for firephp
	 *
	 * @param string message
	 * @param integer message type (0 = system log, 1 = email, 2 = firephp, 3 = file, 4 = sapi log) 2 is default
	 * @param (optional) string destination (email address or log file)
	 * @param (optional) array extra mail headers
	 * @return boolean
	 */
	public function log($msg, $type = 2, $dest = null, $extra = null){
		switch($type){
			case 0:
				return(error_log($msg, $type));
				break;
			case 1:
				return(error_log($msg, $type, $dest, $extra));
				break;
			case 2:
				return(self::debug($msg));
				break;
			case 3:
				return(error_log($msg, $type, $dest));
				break;
			case 4:
				return(error_log($msg, $type));
				break;
		}
		return(false);
	}

	/**
	 * output debug messages to firephp (if installed)
	 *
	 * @param string message
	 * @return boolean
	 */
	public function debug($msg){
		if (self::$config['debug']){
			if (self::$firephp != null && self::$firephp !== false){
				self::$firephp->log($msg, 'Frameworky');
				return(true);
			} else {
				self::lib('firephp.php');
				if (class_exists('FirePHP')){
					self::$firephp = FirePHP::getInstance(true);
					self::$firephp->log($msg, 'Frameworky');
					return(true);
				} else {
					self::$firephp = false;
				}
			}
		}
		return(false);
	}

	/**
	 * determine if a file path is writable or not
	 *
	 * note: the built-in is_writable function fails on files that don't
	 * already exist but can be written to
	 *
	 * @param string file path
	 * @return boolean
	 */
	public function is__writable($path) {
		if ($path{strlen($path)-1}=='/') // recursively return a temporary file path
			return self::is__writable($path.uniqid(mt_rand()).'.tmp');
		else if (is_dir($path))
			return self::is__writable($path.'/'.uniqid(mt_rand()).'.tmp');
		// check tmp file for read/write capabilities
		$rm = file_exists($path);
		$f = @fopen($path, 'a');
		if ($f===false)
			return false;
		fclose($f);
		if (!$rm)
			@unlink($path);
		return true;
	}

	/**
	 * determine if a file is cacheable or not
	 *
	 * note: parent directories will be created if necessary in order to ensure
	 * that a file can be cached properly
	 *
	 * @param string file path
	 * @return boolean success or failure
	 */
	public function is_cacheable($file){
		if (self::is__writable($file)){
			return(true);
		}else{
			// try to write each directory
			$dir = str_replace($_SERVER['DOCUMENT_ROOT'] . '/', '', $file);
			if (ereg('/', $dir)){
				$dir = preg_replace('/^(.*)\/([^\/]+)$/', '$1', $dir);
				$dirs = preg_split('/\//', $dir);
				$buildpath = $_SERVER['DOCUMENT_ROOT'];
				for ($i = 0; $i < count($dirs); $i++){
					if (!is_dir($buildpath . '/' . $dirs[$i])) mkdir($buildpath . '/' . $dirs[$i]);
					$buildpath .= '/' . $dirs[$i];
				}
				if (self::is__writable($file)) return(true);
				else return(false);
			} else {
				return(false);
			}
		}
	}

	/**
	 * output an error file to the current buffer, log it and exit
	 *
	 * @param string error code
	 * @param string Custom error message
	 * @return void
	 */
	public function output_error($code, $msg = '', $line = 0, $file = ''){
		ob_clean();
		$title = '';
		$description = '';
		switch($code){
			case '400':
				$title = 'Bad Request';
				$description = 'This is not a valid HTTP request';
				$description = 'Whatever the hell you\'re doing, stop it!';
				break;
			case '401':
				$title = 'Unauthorized';
				$description = 'User authentication is required before continuing';
				$description = 'You need to take me out for lobster first';
				break;
			case '402':
				$title = 'Payment Required';
				$description = 'User payment is required before continuing';
				$description = 'Give me whatever\'s in your pocket and we\'ll see';
				break;
			case '403':
				$title = 'Forbidden';
				$description = 'User access is denied';
				$description = 'Losers go home!';
				break;
			case '404':
				$title = 'File Not Found';
				$description = 'Requested URI was not found on this server';
				$description = 'Didn\'t find what you were looking for? Hmmm, maybe you should try another URL';
				break;
			case '405':
				$title = 'Method Not Allowed';
				$description = 'Requested method is not supported on this server';
				$description = 'GET, POST, HEAD, those are all valid options ... I don\'t know what in the hell you think you\'re doing';
				break;
			case '406':
				$title = 'Not Acceptable';
				$description = 'Requested URI is of a Content-Type that your browser cannot accept';
				$description = 'Your browser pretty much sucks because it can\'t handle this Content-Type ... maybe it\'s finally time to upgrade';
				break;
			case '407':
				$title = 'Proxy Authentication Required';
				$description = 'Proxy authentication is required before continuing';
				$description = 'Haha! Your IT department wants you to ask permission before allowing you into the proxy server ... do you have the username and password?';
				break;
			case '408':
				$title = 'Request Timeout';
				$description = 'Server timed out waiting for a URI request from the user';
				$description = 'Sorry, I can\'t hold my breath that long. Hit refresh and I\'ll try again.';
				break;
			case '409':
				$title = 'Conflict';
				$description = 'Server refused to service request because of a possible conflict';
				$description = 'Sorry, but this is a conflict of interest for me';
				break;
			case '410':
				$title = 'Gone';
				$description = 'Requested URI was removed and no forwarding address is known';
				break;
			case '411':
				$title = 'Length Required';
				$description = 'Request message was without a required Content-Length';
				break;
			case '412':
				$title = 'Precondition Failed';
				$description = 'Request did not pass servers definition of a valid HTTP request';
				break;
			case '413':
				$title = 'Request Entity Too Large';
				$description = 'Request entity is larger than the server is able or willing to process';
				break;
			case '414':
				$title = 'Request-URI Too Long';
				$description = 'Request-URI is longer than the server is willing to interpret';
				break;
			case '415':
				$title = 'Unsupported Media Type';
				$description = 'Server refused to service request for an unsupported format';
				break;
			case '416':
				$title = 'Requested Range Not Satisfiable';
				$description = 'Server refused to service a Range request-header because the range being requested is not valid';
				break;
			case '417':
				$title = 'Expectation Failed';
				$description = 'Server refused to service an Expect request-header because the server would have been unable to meet the expectation';
				break;
			case '500':
				$title = 'Internal Server Error';
				$description = 'Server encountered an unexpected condition';
				break;
			case '501':
				$title = 'Not Implemented';
				$description = 'Server does not support the functionality required to fulfill the request';
				break;
			case '502':
				$title = 'Bad Gateway';
				$description = 'Server received an invalid response from the upstream server while trying to fulfill your request as an intermediate gateway or proxy';
				break;
			case '503':
				$title = 'Service Unavailable';
				$description = 'Server is unable to fulfill requests due to either a temporary server overload or a maintenance task';
				break;
			case '504':
				$title = 'Gateway Timeout';
				$description = 'Server received a timeout response from the upstream server while trying to fulfill your request as an intermediate gateway or proxy';
				break;
			case '505':
				$title = 'HTTP Version Not Supported';
				$description = 'Server does not support the HTTP protocol version used in this request';
				break;
		}
		if ($title != ''){
			header('HTTP/1.1 ' . $code . ' ' . $title);
			echo '<h1>' . $code . ': ' . $title . '</h1>';
			echo '<p>' . $description . '</p>';
			echo '<table border="1">';
			echo '<tr><td>msg</td><td>' . $msg . '</td></tr>';
			echo '<tr><td>file</td><td>' . str_replace(getenv('DOCUMENT_ROOT'), '', $file) . '</td></tr>';
			echo '<tr><td>line</td><td>' . $line . '</td></tr>';
			echo '</table>';
		}
		if ($msg != '') self::log($code . ': ' . $line . ': ' . $file . ': ' . $msg, 0);
		exit;
	}

	/**
	 * resize an image
	 *
	 * @param string original file
	 * @param integer new width
	 * @param integer new height
	 * @return object resized image
	 */
	public function resize_image($file, $new_width = null, $new_height = null){
		list($width, $height) = getimagesize($file);
		$adjusted_width = ($new_width != null) ? $new_width : $width;
		$adjusted_height = ($new_height != null) ? $new_height : $height;
		if (($new_width != null) && ($new_height == null)) $adjusted_height = ($new_width >= $width) ? (int) (($new_width / $width) * $height) : (int) ($height / ($width / $new_width));
		else if (($new_height != null) && ($new_width == null)) $adjusted_width = ($new_height >= $height) ? (int) (($new_height / $height) * $width) : (int) ($width / ($height / $new_height));
		$new_file = imagecreatetruecolor($adjusted_width, $adjusted_height);
		if (preg_match('/\.jpe*g$/', $file)){
			$old_file = imagecreatefromjpeg($file);
			imagecopyresampled($new_file, $old_file, 0, 0, 0, 0, $adjusted_width, $adjusted_height, $width, $height);
		} else if (preg_match('/\.gif$/', $file)){
			$old_file = imagecreatefromgif($file);
			imagecopyresampled($new_file, $old_file, 0, 0, 0, 0, $adjusted_width, $adjusted_height, $width, $height);
		} else if (preg_match('/\.png$/', $file)){
			if ($adjusted_width != $width && $adjusted_height != $height){
				$old_file = imagecreatefrompng($file);
				$background = imagecolorallocate($new_file, 0, 0, 0);
				imagecolortransparent($new_file, $background);
				imagealphablending($new_file, false);
				imagecopyresampled($new_file, $old_file, 0, 0, 0, 0, $adjusted_width, $adjusted_height, imagesx($old_file), imagesy($old_file));
			} else {
				$old_file = imagecreatefrompng($file);
				imagealphablending($old_file, true);
				imagesavealpha($old_file, true);
				$new_file = $old_file;
			}
		}
		return($new_file);
	}

	/**
	 * write ttl file
	 *
	 * @param string void
	 * @return boolean
	 */
	public function write_ttl_file(){
		$filename = preg_replace('/^.*\/([^\/]+)$/', '$1', self::$requested_uri['uri']);
		$ttl_file = self::$config['www_dir'] . str_replace($filename, '.' . $filename . '.ttl', self::$requested_uri['uri']);
		$fp = fopen($ttl_file, 'w');
		fwrite($fp, time() + self::$processed_uri['ttl']);
		fclose($fp);
	}

	/**
	 * output a binary file to the current buffer with appropriate headers and,
	 * optionally, cache it for next time (if applicable)
	 *
	 * @param void
	 * @param boolean whether to cache file or not
	 * @return void
	 */
	public function output_binary_file(){
		if (in_array(self::$requested_uri['mimetype'], array('image/jpeg','image/png','image/gif')) && (self::$requested_uri['special']['width'] != -1 || self::$requested_uri['special']['height'] != -1 || self::$requested_uri['special']['quality'] < 100)){
			// always cache generated images to reduce server overhead
			if (self::is_cacheable(self::$config['www_dir'] . self::$requested_uri['path'])){
				if (preg_match('/<\?php/', file_get_contents(self::$config['host_dir'] . self::$processed_uri['path']))){
					// capture generated binary file output
					ob_clean();
					ob_start();
					include(self::$config['host_dir'] . self::$processed_uri['path']);
					$ob = ob_get_contents();
					ob_end_clean();

					// write to cache file location
					$fp = fopen(self::$config['www_dir'] . self::$requested_uri['uri'], 'wb');
					fwrite($fp, $ob);
					fclose($fp);
					self::write_ttl_file();

					// output this time for user (next time will be done by apache)
					header('Content-Type: ' . self::$requested_uri['mimetype']);
					header('Content-Length: ' . filesize(self::$config['www_dir'] . self::$requested_uri['path']));
					readfile(self::$config['www_dir'] . self::$requested_uri['path']);
				} else {
					$new_file = self::resize_image(self::$config['host_dir'] . self::$processed_uri['path'], (self::$requested_uri['special']['width'] != -1) ? self::$requested_uri['special']['width'] : null, (self::$requested_uri['special']['height'] != -1) ? self::$requested_uri['special']['height'] : null);
					if (self::$requested_uri['mimetype'] == 'image/jpeg') imagejpeg($new_file, self::$config['www_dir'] . self::$requested_uri['uri'], self::$requested_uri['special']['quality']);
					else if (self::$requested_uri['mimetype'] == 'image/png') imagepng($new_file, self::$config['www_dir'] . self::$requested_uri['uri'], self::$requested_uri['special']['quality'] * .01 * .9);
					else if (self::$requested_uri['mimetype'] == 'image/gif') imagegif($new_file, self::$config['www_dir'] . self::$requested_uri['uri']);
					ob_clean();
					self::write_ttl_file();
					header('Content-Type: ' . self::$requested_uri['mimetype']);
					header('Content-Length: ' . filesize(self::$config['www_dir'] . self::$requested_uri['uri']));
					readfile(self::$config['www_dir'] . self::$requested_uri['uri']);
				}
			} else {
				self::output_error('500', 'The file (' . self::$config['www_dir'] . self::$requested_uri['path'] . ') is not writable', __LINE__, __FILE__);
			}
		} else if (self::$config['cache_enabled'] && self::$processed_uri['ttl'] != -1){
			if (self::is_cacheable(self::$config['www_dir'] . self::$requested_uri['path'])){
				copy(self::$config['host_dir'] . self::$processed_uri['path'], self::$config['www_dir'] . self::$requested_uri['uri']);
				self::write_ttl_file();
				header('Content-Type: ' . self::$requested_uri['mimetype']);
				header('Content-Length: ' . filesize(self::$config['www_dir'] . self::$requested_uri['uri']));
				readfile(self::$config['www_dir'] . self::$requested_uri['uri']);
			} else {
				self::output_error('500', 'The file (' . self::$config['www_dir'] . self::$requested_uri['uri'] . ') is not writable', __LINE__, __FILE__);
			}
		} else {
			header('Content-Type: ' . self::$requested_uri['mimetype']);
			header('Content-Length: ' . filesize(self::$config['host_dir'] . self::$processed_uri['path']));
			readfile(self::$config['host_dir'] . self::$processed_uri['path']);
		}
		//ob_clean();
	}

	/**
	 * output a processed file to the current buffer with appropriate headers
	 * and, optionally, cache the results for next time (if applicable)
	 *
	 * @param void
	 * @return void
	 */
	public function output_processed_file(){
		$content_type = (self::$requested_uri['mimetype'] == 'text/plain' && self::$processed_uri['mimetype'] != 'text/plain') ? self::$processed_uri['mimetype'] : self::$requested_uri['mimetype'];

		if (self::$config['cache_enabled'] && self::$processed_uri['ttl'] != -1){
			if (self::is_cacheable(self::$config['www_dir'] . self::$requested_uri['path'])){
				ob_clean();
				ob_start();
				include(self::$config['host_dir'] . self::$processed_uri['path']);
				$ob = ob_get_contents();
				ob_end_clean();

				if (self::$config['compress_js'] && (in_array('application/x-javascript', array(self::$requested_uri['mimetype'], self::$processed_uri['mimetype'])) || in_array('text/x-json', array(self::$requested_uri['mimetype'], self::$processed_uri['mimetype'])))){
					self::lib('jsmin.php');
					$ob = JSMin::minify($ob);
				} else if (self::$config['compress_css'] && (self::$requested_uri['mimetype'] == 'text/css')){
					self::lib('jsmin.php');
					$ob = JSMin::minify($ob);
				}
				$handle = fopen(self::$config['www_dir'] . self::$requested_uri['path'], 'w');
				if (fwrite($handle, $ob) === false) self::log('Cannot write to file (' . self::$config['www_dir'] . self::$requested_uri['path'] . ')', 0);
				fclose($handle);
				self::write_ttl_file();
				header('Content-Type: ' . $content_type);
				header('Content-Length: ' . filesize(self::$config['www_dir'] . self::$requested_uri['path']));
				self::debug('Content-Execution: ' . self::get_current_execution_time() . 's');
				readfile(self::$config['www_dir'] . self::$requested_uri['path']);
				return(true);
			} else {
				self::output_error('500', 'The file (' . self::$config['www_dir'] . self::$requested_uri['path'] . ') is not writable', __LINE__, __FILE__);
				return(false);
			}
		}

		// load without caching
		ob_clean();
		header('Content-Type: ' . $content_type);
		if (!isset($ob)){
			ob_start();
			include(self::$config['host_dir'] . self::$processed_uri['path']);
			$ob = ob_get_contents();
			ob_end_clean();
		}
		self::debug('Content-Execution: ' . self::get_current_execution_time() . 's');
		echo $ob;
		return(true);
	}

	/**
	 * output a file to the current buffer with appropriate headers and,
	 * optionally, cache it for next time (if applicable)
	 *
	 * @param void
	 * @return boolean success or not
	 */
	public function output_file(){
		if (self::$processed_uri['filetype'] == 'binary') self::output_binary_file();
		else self::output_processed_file();
	}

	/**
	 * get recursive file listing for a given directory
	 *
	 * returned results
	 * [
	 *     'path' => '/path/to/asset'
	 *     'type' => 'file|directory'
	 * ]
	 *
	 * @param string source path
	 * @param (optional) string pattern to match against filenames (directories will be ommitted too)
	 * @return associative array of results, false if path is not found
	 */
	function get_file_listing($path, $pattern = false){
		$path_tree = array();
		if(substr($path, -1) == '/') $path = substr($path,0,-1);
		if(!file_exists($path) || !is_dir($path)) return false;
		else if (is_readable($path)) {
			$path_list = opendir($path);
			while (false !== ($name = readdir($path_list))){
				if (!in_array($name, array('.', '..', '.svn', '.frameworky'))){
					$newpath = $path . '/' . $name;
					if(is_readable($newpath)){
						$subdirectories = explode('/',$newpath);
						if (is_dir($newpath)){
							if ($pattern === false) $path_tree[] = array('path' => $newpath, 'type' => 'directory');
							$nested = self::get_file_listing($newpath, $pattern);
							if ($nested != false){
								$path_tree = array_merge($path_tree, $nested);
							}
						} else if (is_file($newpath)){
							if ($pattern === false || preg_match($pattern, $name)){
								$path_tree[] = array('path' => $newpath, 'type' => 'file');
							}
						}
					}
				}
			}
			closedir($path_list);
			return $path_tree;
		} else return false;
	}

	/**
	 * reset ttl files by removing any which have expired
	 *
	 * @param (optional) string working path
	 * @return string status message
	 */
	public function reset_ttl_files($path = null){
		$count = 0;
		$path = ($path == null) ? self::$config['www_dir'] : $path;
		$ttl_files = self::get_file_listing($path, '/^\..*\.ttl$/');
		if (count($ttl_files) > 0){
			$len = count($ttl_files);
			for ($i = 0; $i < count($ttl_files); $i++){
				$ttl_name = end(explode('/', $ttl_files[$i]['path']));
				$real_name = preg_replace('/^\.(.*)\.ttl$/', '$1', $ttl_name);
				$real_path = substr($ttl_files[$i]['path'], 0, strlen($ttl_files[$i]['path']) - strlen($ttl_name) - 1);
				if (is_file($real_path . '/' . $real_name)){
					if (time() > (int) file_get_contents($ttl_files[$i]['path'])){
						unlink($real_path . '/' . $real_name);
						unlink($ttl_files[$i]['path']);
						$count++;
					}
				}
			}
		}
		return('Reset ' . $count . ' files');
	}
}

class FrameworkyException extends Exception {}

function run($uri){
	if (!isset($GLOBALS['Frameworky'])) $GLOBALS['Frameworky'] = new Frameworky();
	$GLOBALS['Frameworky']->run($uri);
}

?>
